package org.hawk.gwt.ppc.introspection.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.hawk.gwt.ppc.ProcessingUnitsManager;
import org.hawk.gwt.ppc.introspection.IntrospectionException;
import org.hawk.gwt.ppc.introspection.IntrospectionUtils;
import org.hawk.gwt.ppc.introspection.JavaMethod;
import org.hawk.gwt.ppc.introspection.JavaModifier;
import org.hawk.gwt.ppc.introspection.JavaScope;
import org.hawk.gwt.ppc.introspection.JavaTypeScope;
import org.hawk.gwt.ppc.introspection.JavaTypeVariable;
import org.hawk.gwt.ppc.introspection.JavaVariable;
import org.hawk.gwt.ppc.parser.BlockDescriptor;
import org.hawk.gwt.ppc.parser.ClassDescriptor;
import org.hawk.gwt.ppc.parser.CompilationUnitDescriptor;
import org.hawk.gwt.ppc.parser.Descriptor;
import org.hawk.gwt.ppc.parser.DescriptorVisitor;
import org.hawk.gwt.ppc.parser.DoWhileExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ForExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ForeachExpressionDescriptor;
import org.hawk.gwt.ppc.parser.IfExpressionDescriptor;
import org.hawk.gwt.ppc.parser.InitializerExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ModificatedDescriptor;
import org.hawk.gwt.ppc.parser.NewOperatorExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ParameterizedMemberDescriptor;
import org.hawk.gwt.ppc.parser.SwitchExpressionDescriptor;
import org.hawk.gwt.ppc.parser.Token;
import org.hawk.gwt.ppc.parser.TryCatchFinallyExpressionDescriptor;
import org.hawk.gwt.ppc.parser.VariableDescriptor;
import org.hawk.gwt.ppc.parser.WhileExpressionDescriptor;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Implementation of {@link JavaScope} interface.
 * @author alex.bereznevatiy@gmail.com
 *
 */
class JavaScopeImpl extends AbstractJavaStatement implements JavaScope {
	
	private Descriptor descriptor;
	private Map<String, List<JavaVariableImpl>> variables;
	private JavaScopeImpl parent;
	private List<JavaScope> children;

	JavaScopeImpl() {
		variables = new HashMap<String, List<JavaVariableImpl>>();
		children = new ArrayList<JavaScope>();
	}
	
	/**
	 * Initializes this scope with descriptor and parent
	 * @param descriptor
	 * @param parent
	 */
	void init(Descriptor descriptor, JavaScopeImpl parent) {
		this.parent = parent;
		this.descriptor = descriptor;
		if (descriptor == null) {
			return;
		}
		parseChildren(descriptor);
		setModifiers(descriptor);
		if (descriptor instanceof ParameterizedMemberDescriptor) {
			for (VariableDescriptor var : ((ParameterizedMemberDescriptor)descriptor).getParameters()) {
				addVariable(this, var);
			}
		}
	}

	void setModifiers(Descriptor descriptor) {
		if (!(descriptor instanceof ModificatedDescriptor)) {
			setModifiers(Collections.<JavaModifier>emptyList());
			return;
		}
		setModifiers(fromStrings(((ModificatedDescriptor)descriptor).getModifiers()));
	}

	/**
	 * Parses children for this scope using descriptor passed.
	 * @param descriptor
	 */
	void parseChildren(Descriptor descriptor) {
		if (descriptor instanceof BlockDescriptor) {
			parseChildrenBlock((BlockDescriptor)descriptor);
		} else if (descriptor instanceof DoWhileExpressionDescriptor) {
			parseExpression(((DoWhileExpressionDescriptor)descriptor).getExpression());
		} else if (descriptor instanceof WhileExpressionDescriptor) {
			parseExpression(((WhileExpressionDescriptor)descriptor).getExpression());
		} else if (descriptor instanceof ForExpressionDescriptor) {
			ForExpressionDescriptor forDescr = (ForExpressionDescriptor)descriptor;
			addVariable(this, forDescr.getInitializer());
			parseExpression(forDescr.getExpression());
		} else if (descriptor instanceof ForeachExpressionDescriptor) {
			parseExpression(((ForeachExpressionDescriptor)descriptor).getExpression());
		} else if (descriptor instanceof SwitchExpressionDescriptor) {
			parseSwitchExpression((SwitchExpressionDescriptor)descriptor);
		} else if (descriptor instanceof InitializerExpressionDescriptor) {
			parseInitializer((InitializerExpressionDescriptor)descriptor);
		} else {
			throw new IntrospectionException("Unsupported descriptor: " + descriptor);
		}
	}

	private void addVariable(JavaScopeImpl target, ExpressionDescriptor expression) {
		if (target == null) {
			target = this;
		}
		if (expression == null) {
			return;
		}
		final JavaScopeImpl targetScope = target;
		try {
			expression.visit(new DescriptorVisitor() {
				
				public void visit(Descriptor descriptor) throws PreprocessorException {
					if (descriptor instanceof VariableDescriptor) {
						addVariable(targetScope, (VariableDescriptor)descriptor);
					}
				}
			});
		} catch(PreprocessorException ex) {
			throw new IntrospectionException(ex);
		}
	}

	private void addVariable(final JavaScopeImpl targetScope, VariableDescriptor descriptor) {
		JavaVariableImpl var = new JavaVariableImpl(descriptor, targetScope);
		
		List<JavaVariableImpl> vars = targetScope.variables.get(var.getName());
		if (vars == null) {
			vars = new ArrayList<JavaVariableImpl>(1);
			targetScope.variables.put(var.getName(), vars);
		}
		vars.add(var);
	}

	/**
	 * Adds child scope to this one.
	 * @param child
	 */
	void addChild(JavaScopeImpl child) {
		children.add(child);
	}

	private void parseChildrenBlock(BlockDescriptor descriptor) {
		Iterator<Descriptor> iterator = descriptor.iterator();
		while (iterator.hasNext()) {
			addChild(iterator.next());
		}
	}

	private JavaScopeImpl addChild(Descriptor child) {
		if (child == null) {
			return null;
		}
		if (isScope(child)) {
			return addScope(child);
		}
		if (child instanceof ExpressionDescriptor) {
			final ExpressionDescriptor expr = (ExpressionDescriptor)child;
			addVariable(this, expr);
			
			try {
				expr.visit(new DescriptorVisitor() {
					
					public void visit(Descriptor descriptor) throws PreprocessorException {
						if (!(descriptor instanceof ExpressionDescriptor) ||
								!isParent((ExpressionDescriptor)descriptor, expr)) {
							return;
						}
						if (descriptor instanceof NewOperatorExpressionDescriptor) {
							NewOperatorExpressionDescriptor newOperator = (NewOperatorExpressionDescriptor) descriptor;
							if (newOperator.getAnonymousClass() != null) {
								addChild(newOperator.getAnonymousClass());
							}
						}
					}

					private boolean isParent(ExpressionDescriptor descriptor,
							ExpressionDescriptor parent) {
						while (descriptor != null) {
							if (descriptor == parent) {
								return true;
							}
							descriptor = descriptor.getParent();
						}
						return false;
					}
				});
			} catch(PreprocessorException ex) {
				throw new IntrospectionException(ex);
			}
		} else if (child instanceof ClassDescriptor) {
			addChild((JavaScopeImpl)IntrospectionUtils.getType((ClassDescriptor)child));
		}
		return this;
	}

	private JavaScopeImpl addScope(Descriptor child) {
		JavaScopeImpl scope = new JavaScopeImpl();
		scope.init(child, this);
		addChild(scope);
		return scope;
	}

	private boolean isScope(Descriptor child) {
		return child instanceof BlockDescriptor || 
				child instanceof DoWhileExpressionDescriptor || 
				child instanceof WhileExpressionDescriptor || 
				child instanceof ForExpressionDescriptor || 
				child instanceof ForeachExpressionDescriptor || 
				child instanceof SwitchExpressionDescriptor || 
				child instanceof InitializerExpressionDescriptor;
	}

	private void parseExpression(ExpressionDescriptor descriptor) {
		if (descriptor instanceof IfExpressionDescriptor) {
			IfExpressionDescriptor expr = (IfExpressionDescriptor)descriptor;
			addChild(expr.getExpression());
			addChild(expr.getElseExpression());
		} else if (descriptor instanceof TryCatchFinallyExpressionDescriptor) {
			TryCatchFinallyExpressionDescriptor tryDescr = (TryCatchFinallyExpressionDescriptor)descriptor;
			addChild(tryDescr.getTryBlock());
			for (ExpressionDescriptor catchClause : tryDescr.getCatchClasuses()) {
				addVariable(addChild(tryDescr.getCatchBlock(catchClause)), catchClause);
			}
			addChild(tryDescr.getFinallyBlock());
		} else if (descriptor instanceof BlockDescriptor) {
			parseChildrenBlock((BlockDescriptor)descriptor);
		} else {
			addChild(descriptor);
		}
	}

	private void parseSwitchExpression(SwitchExpressionDescriptor descriptor) {
		for (ExpressionDescriptor expr : descriptor.getCases()) {
			for (ExpressionDescriptor sub : descriptor.getCaseExpression(expr)) {
				addVariable(addChild(sub), expr);
			}
		}
		
	}

	private void parseInitializer(InitializerExpressionDescriptor descriptor) {
		for (int i = 0; i < descriptor.getSize(); i++) {
			addChild(descriptor.get(i));
		}
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaScope#getScope(org.hawk.gwt.ppc.parser.Token)
	 */
	public JavaScope getScope(Token marker) {
		return getScope(marker.getLine(), marker.getOffset());
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaScope#getScope(int, int)
	 */
	public JavaScope getScope(int line, int offset) {
		if (descriptor == null || !descriptor.hasToken(line, offset)) {
			return null;
		}
		for (JavaScope scope : getChildren()) {
			JavaScope result = scope.getScope(line, offset);
			if (result != null) {
				return result;
			}
		}
		return this;
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaScope#getVariable(java.lang.String, org.hawk.gwt.ppc.parser.Token)
	 */
	public JavaVariable getVariable(String name, Token marker) {
		return getVariable(name, marker.getLine(), marker.getOffset());
	}
		
	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaScope#getVariable(java.lang.String, int, int)
	 */
	public JavaVariable getVariable(String name, int line, int offset) { 
		List<JavaVariableImpl> variables = this.variables.get(name);
		JavaVariableImpl result = null;
		if (variables == null) {
			return parent == null ? null : parent.getVariable(name, line, offset);
		}
		for (JavaVariableImpl var : variables) {
			Token tok = var.getDescriptor().getFirstToken();
			if (tok.getLine() > line || 
					(tok.getLine() == line && 
					tok.getOffset() > offset)) {
				continue;
			}
			if (result == null) {
				result = var;
				continue;
			}
			Token resultToken = result.getDescriptor().getFirstToken();
			Token varToken = var.getDescriptor().getFirstToken();
			if (varToken.getLine() < resultToken.getLine() || 
				(varToken.getLine() == resultToken.getLine() &&
				varToken.getOffset() > resultToken.getOffset())) {
				result = var;
			}
		}
		if (result == null) {
			return parent == null ? null : parent.getVariable(name, line, offset);
		}
		return result;
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaScope#getParent()
	 */
	public JavaScope getParent() {
		return parent;
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaScope#getChildren()
	 */
	public List<JavaScope> getChildren() {
		return children;
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaScope#getBestMatchMethod(java.lang.String, java.util.List)
	 */
	public JavaMethod getBestMatchMethod(String name,
			List<JavaTypeScope> arguments) {
		if (parent == null) {
			return null;
		}
		return parent.getBestMatchMethod(name, arguments);
	}

	/**
	 * Internal type resolution method to deal with type variables.
	 * @param name
	 * @param context
	 * @return type with name specified (never <code>null</code>).
	 */
	JavaTypeScope resolveType(String name, CompilationUnitDescriptor context) {
		if (parent != null) {
			return parent.resolveType(name, context);
		}
		try {
			name = Name.getRawName(name);
			return IntrospectionUtils.resolveType(name, context, ProcessingUnitsManager.getInstance().getRepository());
		} catch(PreprocessorException ex) {
			throw new IntrospectionException(ex);
		}
	}
	
	/**
	 * @return compilation unit for source descriptor or <code>null</code>.
	 */
	CompilationUnitDescriptor getCompilationUnit() {
		return descriptor == null ? null : descriptor.getCompilationUnit();
	}
	
	/**
	 * @return descriptor associated with this scope.
	 */
	Descriptor getDescriptor() {
		return descriptor;
	}

	/**
	 * @param paramName
	 * @return type parameter for given type
	 */
	JavaTypeVariable resolveTypeParameter(String paramName) {
		if (parent != null) {
			return parent.resolveTypeParameter(paramName);
		}
		return null;
	}
}
